package com.example.commons.redis;

import com.example.commons.exception.BusinessException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Configuration;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

@Configuration
public class RedisLuaLock {
    @Autowired
    @Qualifier("jedisPool")
    private JedisPool jedisPool;


    private final int DB = 0;
    private final String LOCK_KEY = "REDIS:LUA:LOCK:%s";

    private volatile boolean running = true;

    //加锁
    private static final String LOCK_SCRIPT =
            "if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then " +
                    "   redis.call('expire', KEYS[1], ARGV[2]) " +
                    "   return 1 " +
                    "else " +
                    "   return 0 " +
                    "end";

    //超时续期
    private static final String RENEW_LOCK_SCRIPT =
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                    "   return redis.call('expire', KEYS[1], ARGV[2]) " +
                    "else " +
                    "   return 0 " +
                    "end";

    //解锁
    private static final String RELEASE_LOCK_SCRIPT =
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                    "   return redis.call('del', KEYS[1]) " +
                    "else " +
                    "   return 0 " +
                    "end";


    private Jedis getJedis(int db) {
        Jedis jedis = jedisPool.getResource();
        jedis.select(db);
        return jedis;
    }


    /**
     * @param key         业务key
     * @param expireTime  超时时间/ms
     * @param waitTimeout 等待时间，超过等待时间就放弃/ms
     * @param val         uuid
     *                    jedis.eval(final String script, final List<String> keys, final List<String> args)
     *                    入参说明：
     *                    script：执行脚本
     *                    keys：key
     *                    args：参数，与脚本对应
     * @return
     */
    public boolean tryLock(String key, String val, long expireTime, long waitTimeout) {
        this.check(expireTime, waitTimeout);
        key = String.format(LOCK_KEY, key);
        val = Thread.currentThread().getId() + "-" + val;
        List<String> args = new ArrayList<>();
        args.add(val);
        args.add(String.valueOf(expireTime));
        try (Jedis jedis = this.getJedis(DB)) {
            //计算等待超时时间，超过时间直接返回获取锁失败
            long deadTimeLine = System.currentTimeMillis() + waitTimeout;
            //是否继续获取锁
            boolean flag = true;
            //锁是否获取成功
            boolean result = false;
            while (flag) {
                if (1 == (long) jedis.eval(LOCK_SCRIPT, Collections.singletonList(key), args)) {
                    flag = false;
                    result = true;
                    //todo 守护
                    System.out.println(val + "加锁成功，开始守护");
                    this.guardThread(key, expireTime, val);
                } else {
                    waitTimeout = deadTimeLine - System.currentTimeMillis();
                    // 超过等待时间仍然没有成功获取锁的话就放弃，这里判断大于100是因为下面睡眠了100，如果小于100，直接返回获取锁失败
                    if (waitTimeout > 100L) {
                        try {
                            //防止一直获取锁
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        flag = true;
                    } else {
                        flag = false;
                    }
                    result = false;
                }
            }
            return result;
        }
    }


    /**
     * 看门狗机制，开启守护进程，进行key续期
     *
     * @param key
     * @param expireTime
     * @param val
     */
    private void guardThread(String key, long expireTime, String val) {
        this.running = true;
        Thread thread = new Thread(() -> {
            System.out.println("进来");
            try {
                //先睡眠一下，防止一进来就续期
                Thread.sleep(expireTime - 500);
            } catch (Exception e) {
                e.printStackTrace();
                return;
            }

            List<String> args = new ArrayList<>();
            args.add(val);
            args.add(String.valueOf(expireTime));
            while (this.running) {
                try (Jedis jedis = this.getJedis(DB)) {
                    if (jedis.exists(key)) {
                        //续期
                        if (1 != (long) jedis.eval(RENEW_LOCK_SCRIPT, Collections.singletonList(key), args)) {
                            this.running = false;
                        } else {
                            try {
                                Thread.sleep(expireTime - 500);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                                this.running = false;
                            }
                        }
                    } else {
                        this.running = false;
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                    this.running = false;
                }
            }
        });
        //java中线程分为两种类型：用户线程和守护线程。通过Thread.setDaemon(false)设置为用户线程；通过Thread.setDaemon(true)设置为守护线程。如果不设置次属性，默认为用户线程。
        thread.setDaemon(true);
        thread.start();
    }

    private void threadStop() {
        this.running = false;
    }

    public void unlock(String key, String val) {
        this.threadStop();
        key = String.format(LOCK_KEY, key);
        val = Thread.currentThread().getId() + "-" + val;
        try (Jedis jedis = this.getJedis(DB)) {
            jedis.eval(RELEASE_LOCK_SCRIPT, Collections.singletonList(key), Collections.singletonList(val));
        }
    }

    //检查时间是否规范
    private void check(long expireTime, long waitTimeout) {
        if (0 >= waitTimeout) {
            throw new BusinessException("超时时间有误");
        }
        if (500 >= expireTime) {
            throw new BusinessException("时间应大于500ms");
        }
    }


}
